<script setup>
import { ref } from 'vue';

const props = defineProps({
    class: String,
});

const textAreaRef = ref();

const handleClickWrapper = (event) => {
    if (event.target !== event.currentTarget) return;
    textAreaRef.value.focus();
};

const getContent = () => {
    const children = Array.from(textAreaRef.value.childNodes);
    return children.map((e) => e.textContent);
};

const INITIAL_TEXT_AREA_CONTENT = '<div>&nbsp;</div>';

const reset = () => {
    textAreaRef.value.innerHTML = INITIAL_TEXT_AREA_CONTENT;
};

const DEFAULT_TEXT_COLOR = 'black';
const ERROR_TEXT_COLOR = '#42b883';

const setElementColor = (element, color) => {
    element.style.color = color;
    // 经历换行再合并后，div 内会有多个 span，设置颜色时需要递归一层的进行设置
    const children = Array.from(element.children);
    if (children.length > 0) {
        children.forEach((child) => {
            child.style.color = color;
        });
    }
};

const setInvalidStyleByIndexes = (indexes) => {
    const children = Array.from(textAreaRef.value.children);

    children.forEach((e, i) => {
        setElementColor(e, indexes.includes(i) ? ERROR_TEXT_COLOR : DEFAULT_TEXT_COLOR);
    });
};

const handlePaste = (event) => {
    event.preventDefault();
    const pasteContent = event.clipboardData.getData('text/plain');
    // TODO: `execCommand` is deprecated. if using Firefox, maybe out of expectation
    document.execCommand('insertText', false, pasteContent);
};

/**
 * 向上回溯找到行的 div 对应的 Node，通过判断 node.nodeName
 * 如果行 div 里仅有 text，则回溯路径：#text -> DIV
 * 如果行 div 包含了 span，则回溯路径：#text -> SPAN -> DIV
 */
const findRowNode = (node) => {
    let targetNode = node;
    while (targetNode.nodeName !== 'DIV') {
        targetNode = targetNode.parentNode;
    }
    return targetNode;
};

const handleNewLine = () => {
    // 经过换行再合并会在 div 里产生 span
    const { anchorNode } = window.getSelection();
    const rowNode = findRowNode(anchorNode);
    const textAreaNode = rowNode.parentNode;
    const index = Array.from(textAreaNode.childNodes).findIndex((n) => n === rowNode);
    // 换行后，光标停留的行为新行，清除其样式
    const children = Array.from(textAreaRef.value.children);
    setElementColor(children[index], DEFAULT_TEXT_COLOR);
};

const handleDelete = () => {
    if (textAreaRef.value.innerHTML) return;
    textAreaRef.value.innerHTML = INITIAL_TEXT_AREA_CONTENT;
};

const handleKeyPress = (event) => {
    // event.target === event.currentTarget
    if (event.code === 'Enter') {
        handleNewLine();
        return;
    }
    if (event.code === 'Backspace') {
        handleDelete();
    }
};

defineExpose({ getContent, reset, contentRef: textAreaRef, setInvalid: setInvalidStyleByIndexes });
</script>
<template>
    <div class="wrapper" :class="{ [props.class]: !!props.class }" @click="handleClickWrapper">
        <div ref="textAreaRef" class="text-area" :contenteditable="true" @paste="handlePaste" @keyup="handleKeyPress" v-html="INITIAL_TEXT_AREA_CONTENT"></div>
    </div>
</template>
<style lang="less" scoped>
.wrapper {
    box-sizing: border-box;
    border: 1px solid lightblue;
    border-radius: 8px;
    padding: 8px;
    max-width: 100%;
    overflow-x: scroll;
    overflow-y: auto;
}
.text-area {
    box-sizing: border-box;
    outline: none;
    white-space: pre;
    color: black;
}
</style>
<script>
export default {
    name: 'TextArea',
};
</script>
